// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/okex_v5_futures/api_order/native_private_client.h"

#include <memory>
#include <string>

#include <rapidjson/writer.h>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/okex_common/api_util/rate_tracker.h"
#include "coin2/exchange/okex_futures/symbology/product.h"

#define CHECK_API_LIMIT(action_type, query_type, intrument)                               \
  if (!local_rate_tracker_->HasEnoughRateLimit(ts, action_type, query_type, intrument)) { \
    if (order_logger_cb_) {                                                               \
      rate_tracker_->WriteRateLimitReportMessage(order_logger_cb_);                       \
    }                                                                                     \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled, " << __func__; \
    api_block_info_[__func__]++;                                                          \
    return {};                                                                            \
  }

using coin2::exchange::okex_futures::symbology::OkexFuturesProduct;

auto OkexV5FuturesNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();

  auto product = OkexFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_px(formatter.FormatPrice(order_spec.price));
  params.set_sz(formatter.FormatQty(order_spec.qty));
  params.set_cl_ord_id(GetProcOrderId(proc_order_id));

  params.set_inst_id(order_spec.product);
  params.set_td_mode(td_mode_);

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("buy");
    params.set_reduce_only(order_spec.reduce_only);
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("sell");
    params.set_reduce_only(order_spec.reduce_only);
  } else if (order_spec.order_side == OrderSide::BUY_OPEN_ORDER) {
    params.set_side("buy");
    params.set_pos_side("long");
  } else if (order_spec.order_side == OrderSide::SELL_OPEN_ORDER) {
    params.set_side("sell");
    params.set_pos_side("short");
  } else if (order_spec.order_side == OrderSide::SELL_CLOSE_ORDER) {
    params.set_side("sell");
    params.set_pos_side("long");
  } else if (order_spec.order_side == OrderSide::BUY_CLOSE_ORDER) {
    params.set_side("buy");
    params.set_pos_side("short");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  if (order_spec.post_only) {
    params.set_ord_type("post_only");
  } else if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
    params.set_ord_type("fok");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_ord_type("ioc");
  } else if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_ord_type("market");
  } else {
    params.set_ord_type("limit");
  }

  return params;
}

auto OkexV5FuturesNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();
  params.set_inst_id(order_context.native_product());
  if (order_context.has_exchange_order_id()) {
    params.set_ord_id(order_context.exchange_order_id());
  } else {
    params.set_cl_ord_id(GetProcOrderId(order_context.proc_order_id()));
  }
  return params;
}

std::string OkexV5FuturesNativePrivateClient::GetProcOrderId(int64_t proc_order_id) {
  return "pl" + std::to_string(proc_order_id);
}

OkexV5FuturesNativePrivateClient::OkexV5FuturesNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override,
    bool is_swap) {
  mea_ = mea;
  NativeClientConfig config;
  config.set_rest_host("www.okx.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[OkexV5FuturesNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<OkexRestAuth, NativeClientConfig>(config);
  if (is_swap) {
    inst_type_ = "SWAP";
  } else {
    inst_type_ = "FUTURES";
  }

  // Ignore config.use_cross_mode()
  // "isolated" mode is not used.
  td_mode_ = "cross";

  rate_tracker_ = std::make_unique<OkexRateTracker>(mea_, config);
  local_rate_tracker_ = static_cast<OkexRateTracker*>(rate_tracker_.get());
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::QueryAccountInfo() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::BALANCE, "");
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v5/account/balance", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::BALANCE, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::QueryFundBalance() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::BALANCE, "");
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v5/asset/balances", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::BALANCE, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::QueryPositions() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::POSITION, "");
  UrlParamsBuilder builder;
  builder.PutGet("instType", inst_type_);
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v5/account/positions", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::POSITION, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_POSITION;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::QueryOpenOrders(
    std::string_view after_order_id) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::OPEN_ORDER, "");
  UrlParamsBuilder builder;
  builder.PutGet("instType", inst_type_);
  builder.PutGet("limit", "100");
  if (!after_order_id.empty()) {
    builder.PutGet("after", after_order_id.data());
  }
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v5/trade/orders-pending", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::OPEN_ORDER, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::QueryHistoryOrders() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::HIST_ORDER, "");
  UrlParamsBuilder builder;
  builder.PutGet("instType", inst_type_);
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v5/trade/orders-history", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::HIST_ORDER, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_DONE_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::QueryFills() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::FILL_ORDER, "");
  UrlParamsBuilder builder;
  builder.PutGet("instType", inst_type_);
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v5/trade/fills", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::FILL_ORDER, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::PlaceSingleOrder(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  std::string instrument = params.td_mode() + params.inst_id();
  CHECK_API_LIMIT(ActionType::PLACE, QueryType::UNKNOWN, instrument);
  UrlParamsBuilder builder;
  builder.PutPost("instId", params.inst_id());
  builder.PutPost("tdMode", params.td_mode());
  builder.PutPost("ordType", params.ord_type());
  builder.PutPost("side", params.side());
  builder.PutPost("clOrdId", params.cl_ord_id());
  builder.PutPost("px", params.px());
  builder.PutPost("sz", params.sz());

  // only in net mode
  if (params.has_reduce_only()) {
    builder.PutPost<true>("reduceOnly", params.reduce_only());
  }

  // we prefer no pos_side
  if (params.has_pos_side()) {
    builder.PutPost("posSide", params.pos_side());
  }
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/v5/trade/order", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::PLACE, QueryType::UNKNOWN, instrument);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::PlaceBatchOrders(
    const std::vector<RestParams>& params) {
  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartArray();

  for (const auto& param : params) {
    writer.StartObject();

    writer.Key("instId");
    writer.String(param.inst_id().c_str());
    writer.Key("tdMode");
    writer.String(param.td_mode().c_str());
    writer.Key("ordType");
    writer.String(param.ord_type().c_str());
    writer.Key("side");
    writer.String(param.side().c_str());
    writer.Key("sz");
    writer.String(param.sz().c_str());
    writer.Key("px");
    writer.String(param.px().c_str());
    writer.Key("clOrdId");
    writer.String(param.cl_ord_id().c_str());
    // only in net mode
    if (param.has_reduce_only()) {
      writer.Key("reduceOnly");
      writer.Bool(param.reduce_only());
    }
    // we prefer no pos_side
    if (param.has_pos_side()) {
      writer.Key("posSide");
      writer.String(param.pos_side().c_str());
    }

    writer.EndObject();
  }

  writer.EndArray();

  auto* context = static_cast<OkexRestAuth*>(GetAuth())->CreateRequestByPostWithSignature(
      "/api/v5/trade/batch-orders",
      str_buf.GetString());
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::MULTI_SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::CancelSingleOrder(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  std::string instrument = params.td_mode() + params.inst_id();
  CHECK_API_LIMIT(ActionType::CANCEL, QueryType::UNKNOWN, instrument);
  UrlParamsBuilder builder;
  builder.PutPost("instId", params.inst_id());
  CHECK(params.has_ord_id() != params.has_cl_ord_id())
      << "only one of order_id and client_id can be set!" << params.DebugString();

  if (params.has_ord_id() > 0) {
    builder.PutPost("ordId", params.ord_id());
  } else {
    builder.PutPost("clOrdId", params.cl_ord_id());
  }
  auto* context =
      GetAuth()->CreateRequestByPostWithSignature("/api/v5/trade/cancel-order", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::CANCEL, QueryType::UNKNOWN, instrument);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::CancelBatchOrders(
    const std::vector<RestParams>& params) {
  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartArray();

  for (const auto& param : params) {
    writer.StartObject();

    writer.Key("instId");
    writer.String(param.inst_id().c_str());

    if (param.has_ord_id()) {
      writer.Key("ordId");
      writer.String(param.ord_id().c_str());
    } else if (param.has_cl_ord_id()) {
      writer.Key("clOrdId");
      writer.String(param.cl_ord_id().c_str());
    } else {
      NOTREACHED() << "one of ord_id and cl_ord_id must set!";
    }

    writer.EndObject();
  }

  writer.EndArray();

  auto* context = static_cast<OkexRestAuth*>(GetAuth())->CreateRequestByPostWithSignature(
      "/api/v5/trade/cancel-batch-orders",
      str_buf.GetString());
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::MULTI_CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  if (!order_exe.is_place_batch_orders) {
    const auto& order_spec = *order_exe.order_spec;
    const auto proc_order_id = order_exe.proc_order_id;
    auto params = GetRestOrderParams(order_spec, proc_order_id);
    return PlaceSingleOrder(params);
  }

  auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);

  if (mutable_order_exe->place_pending_order_deque.empty()) {
    return {};
  }

  std::vector<RestParams> params;
  for (int i = 0; i < 20 && !mutable_order_exe->place_pending_order_deque.empty(); ++i) {
    const auto* order_spec = mutable_order_exe->place_pending_order_deque.front();
    mutable_order_exe->place_pending_order_deque.pop_front();

    mutable_order_exe->place_proceed_order_deque.push_back(order_spec);

    params.emplace_back(GetRestOrderParams(*order_spec, order_spec->proc_order_id));
  }

  return PlaceBatchOrders(params);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  if (!order_exe.is_cancel_batch_orders) {
    auto params = GetRestCancelParams(*order_exe.oc);
    return CancelSingleOrder(params);
  }

  auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);

  if (mutable_order_exe->cancel_pending_order_deque.empty()) {
    return {};
  }

  std::vector<RestParams> params;
  for (int i = 0; i < 20 && !mutable_order_exe->cancel_pending_order_deque.empty(); ++i) {
    const auto* order_context = mutable_order_exe->cancel_pending_order_deque.front();
    mutable_order_exe->cancel_pending_order_deque.pop_front();

    mutable_order_exe->cancel_proceed_order_deque.push_back(order_context);

    RestParams param = GetRestCancelParams(*order_context);

    params.emplace_back(std::move(param));
  }

  return CancelBatchOrders(params);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::QueryDeposit() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::DEPOSIT, "");
  UrlParamsBuilder builder;
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v5/asset/deposit-history", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::DEPOSIT, "");
  api_req_info_[__func__]++;

  context->query_type = OkexV5FuturesOrderSubTopicId::REST_DEPOSIT;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::QueryWithdrawal() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::WITHDRAWAL, "");
  UrlParamsBuilder builder;
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v5/asset/withdrawal-history", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::WITHDRAWAL, "");
  api_req_info_[__func__]++;

  context->query_type = OkexV5FuturesOrderSubTopicId::REST_WITHDRAWAL;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::QueryFundingFee() {
  UrlParamsBuilder builder;
  builder.PutGet("instType", inst_type_);
  builder.PutGet("type", 8);
  builder.PutGet("limit", 200);
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v5/account/bills", &builder);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_FUNDING_FEE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceSingleOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelSingleOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> OkexV5FuturesNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountInfo();
    }
    case RestQueryProto::QUERY_ACCOUNT_POSITION: {
      return QueryPositions();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return QueryOpenOrders("");
    }
    default: {
      return nullptr;
    }
  }
}
